package com.hmdp.utils;

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.*;

/**
 * @author He Kuntao
 * @desc 缓存处理工具类
 * @date 2024/1/9 21:23
 */
@Slf4j
@Component
public class CacheManager {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);


    /**
     * 设置任意类型缓存
     */
    public void set(String key, Object value, Long time, TimeUnit timeUnit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, timeUnit);
    }

    /**
     * 设置带逻辑过期时间的缓存
     */
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit timeUnit) {
        // 设置逻辑过期对象
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time)));
        // 写入Redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 查询数据-缓存NULL-解决缓存穿透
     */
    public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Long time, TimeUnit timeUnit,
                                          Function<ID, R> dbFallback) {
        String key = keyPrefix + id;
        // 1 从Redis中查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        if (StringUtils.isNotBlank(json)) {
            if (CACHE_NULL.equals(json)) {
                // 缓存命中-空缓存
                return null;
            } else {
                // 缓存命中-正常数据
                return JSONUtil.toBean(json, type);
            }
        }

        // 2 Redis中无缓存，查询数据库
        R result = dbFallback.apply(id);
        if (result == null) {
            // 数据不存在，将空值存入缓存
            stringRedisTemplate.opsForValue().set(key, "null", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        // 数据存在，存入数据
        this.set(key, result, time, timeUnit);
        return result;
    }


    /**
     * 查询数据-逻辑过期-解决缓存击穿
     */
    public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, String lockKey, Long time, TimeUnit timeUnit,
                                            Function<ID, R> dbFallback) {
        String key = keyPrefix + id;
        // 1 查询Redis
        String json = stringRedisTemplate.opsForValue().get(key);
        // 未命中缓存，返回NULL(热点key均提前定义，此处为增强程序硕健性)
        if (StringUtils.isBlank(json)) {
            return null;
        }
        // 命中缓存，判断是否逻辑过期
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        R result = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        // 未过期，直接返回
        if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
            return result;
        }

        // 2 逻辑过期，缓存重建
        boolean isLock = tryLock(lockKey + id);
        if (isLock) {
            // 获取到互斥锁，(应该再次监测Redis缓存是否过期,DoubleCheck)启动新线程，进行缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 查数据库
                    R r = dbFallback.apply(id);
                    // 重建缓存
                    setWithLogicalExpire(key, r, time, timeUnit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    unLock(LOCK_SHOP_KEY + id);
                }
            });
        }

        // 3. 返回过期数据
        return result;
    }


    /**
     * 加互斥锁
     * 一般互斥锁的过期时间要比业务所需重构缓存时间长一点即可
     *
     * @param key 锁名
     * @return 加锁结果
     */
    private boolean tryLock(String key) {
        return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, "lock", LOCK_SHOP_TTL, TimeUnit.SECONDS));
    }


    /**
     * 释放互斥锁
     *
     * @param key 锁名
     */
    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }

}
